// ------------------------------------------------------------------------
// 版权信息
// 版权归重庆虫儿飞科技有限公司所有。
// 所有权利保留。
// 官方网站：https://netokit.com
// 许可证信息
// Neto.Kit 项目主要遵循 MIT 许可证和 Apache 许可证（版本 2.0）进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// 
// 使用条款
// 使用本代码应遵守相关法律法规和许可证的要求。
// 
// 免责声明
// 对于因使用本代码而产生的任何直接、间接、偶然、特殊或后果性损害，我们不承担任何责任。
// 
// 其他重要信息
// Neto.Kit 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。
// 有关 Neto.Kit 项目的其他详细信息，请参阅位于源代码树根目录中的 COPYRIGHT 和 DISCLAIMER 文件。
// 
// 更多信息
// 请访问 https://netokit.com 获取更多关于 Neto.Kit 项目的许可证和版权信息。
// ------------------------------------------------------------------------

#region

using Furion.DatabaseAccessor;
using Furion.DataEncryption;
using Lazy.Captcha.Core;
using Neto.Application.ApiAdmin.Account.Dto;
using Neto.Core.CoreServices;
using Neto.Core.Entities;
using Neto.Db.SqlSugar;
using Neto.Encry;
using Neto.Web;
using System.Data;
using Yitter.IdGenerator;

#endregion

namespace Neto.Application.ApiAdmin.Account;

/// <summary>
///     账户服务
/// </summary>
/// <param name="captcha"></param>
/// <param name="db"></param>
[ApiDescriptionSettings("Api", Name = "ApiAccountService")]
public class ApiAccountService(
    ICaptcha captcha,
    ISqlSugarClient db,
    ISysLogLoginService sysLogLoginService)
    : IDynamicApiController
{
    /// <summary>
    /// 获取用户信息
    /// </summary>
    /// <returns></returns>
    [HttpPost]
    public async Task<ApiLoginOut> GetUserInfo()
    {
        //获取当前登录用户的id
        return await GetLoginUser();
    }


    /// <summary>
    ///     获取当前登录用户
    /// </summary>
    /// <returns></returns>
    private async Task<ApiLoginOut> GetLoginUser()
    {
        var loginUserId = App.HttpContext.GetLoginUserId();
        var user = await db.Queryable<SysUser>().Includes(o => o.Roles)
            .FirstAsync(o => o.Id == loginUserId);
        return user.Adapt<ApiLoginOut>();
    }

    /// <summary>
    ///     管理登录(WebAPI 专用，使用的JWT方式授权验证)
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    [AllowAnonymous]
    [OperationId(nameof(Login))]
    public async Task<ApiLoginOut> Login(ApiLoginInput input)
    {
        ////验证码
        //if (!CaptchaHelper.VerifyCode(input.VerifyCode, verifyKey: "Api"))
        //{
        //    throw Oops.Oh("验证码不正确!");
        //}
        // //验证是否允许登录（账号登录失败次数过多）
        // if (!input.Account.IsAllowLogin("AbcApi", out var errorMsg)) throw Oops.Oh(errorMsg);

        var newlog = new SysLogLoginInput
        {
            Type = LoginUserType.管理后台,
            Account = input.Account,
            Status = LoginLogStatus.成功,
            Result = "登录成功",
            RequestUrl = App.HttpContext.Request.Path,
            Ip = App.HttpContext.GetIpV4(),
            OS = App.HttpContext.GetWebInfo()?.RequestEx.Os,
            Browser = App.HttpContext.GetWebInfo()?.RequestEx.Browser,
        };

        SysUser dbmodel = null;
        bool isIpLock = false;
        try
        {

            #region 图片验证码

            var verifyCodeId = CacheX.Get<string>($"ApiAccountService_GetVerifyCode_{input.VerifyKey}");
            if (!captcha.Validate(verifyCodeId, input.VerifyCode))
                // input.Account.LoginFail("HtAdmin");
                throw Oops.Bah("验证码不正确!");

            #endregion 图片验证码
            var ipLockInfo = await CheckIPLocked(newlog.Ip);
            //判断ip有没有被锁定
            if (ipLockInfo.IsLocked)
            {
                isIpLock = true;
                //被锁定，抛出ip锁定提示语
                throw Oops.Oh("尝试次数过多，请稍后重试");
            }


            var pwd = input.PassWord.ToPBKDF2Hash();
            dbmodel = await db.Queryable<SysUser>()
               //2025年7月1日19:03:25日更新
               //如果IsEnable为false理应等同于不存在这个用户
               .FirstAsync(o => o.Account == input.Account && o.IsEnable == true);
            if (dbmodel == null) throw Oops.Bah("账号或密码错误");



            //判断是否锁定了
            var accountLockInfo = await CheckAccountLocked(dbmodel.Id);
            if (accountLockInfo.IsLocked == true)
            {
                isIpLock = true;
                //如果已经被锁定,把原因提供给用户
                //throw Oops.Oh($"账号已锁定，请{accountLockInfo.UnlockTime}后重试或联系管理员，原因：{accountLockInfo.Reason}");
                //throw Oops.Bah("账号或密码错误");
                throw Oops.Oh("尝试次数过多，请稍后重试");
            }

            #region 解密提交的密码

            var appinfo = AppX.GetConfig<AppInfoOptions>();
            var LoginPriveKey = appinfo.HtAdminLoginRsaPrivate.ToRsaContentFormatClear();
            var LoginPublicKey = appinfo.HtAdminLoginRsaPublic.ToRsaContentFormatClear();
            var rsa = new LoginRsa(LoginPriveKey, LoginPublicKey);
            //解密前端提交的密钥


            var pwdtxt = rsa.Decrypt(input.PassWord);

            #endregion 解密提交的密码

            //if (dbmodel == null)
            //    //记录账号登录失败
            //    //input.Account.LoginFail("AbcApi");
            //    throw Oops.Bah("账号或密码错误");

            if (!pwdtxt.ToPBKDF2Verify(dbmodel.Pwd))
            {

                //记录账号登录失败
                await HandleFailedAccountWithPwd(dbmodel, newlog.Ip);

                throw Oops.Bah("账号或密码错误");
            }

            //密码输入正确后,重置计数器
            dbmodel.LoginFailedCount = 0;

            //if (dbmodel.IsEnable == false)
            //    //记录账号登录失败
            //    //input.Account.LoginFail("AbcApi");
            //    throw Oops.Bah("账号或密码错误");



            dbmodel.LastLoginTime = DateTimeOffset.Now;
            dbmodel.LastLoginIp = newlog.Ip;
            #region 日志

            //var newlog = new WebLog()
            //{
            //    CreationTime = DateTimeOffset.Now,
            //    RemoteAddr = ip,
            //    RequestUrl = _httpContextAccessor?.HttpContext?.Request?.Path,
            //    Par1 = _httpContextAccessor?.HttpContext?.Request?.Headers["User-Agent"].ToString(),
            //    Type = "管理员登录",
            //    LogContent = $"登录账号：{dbmodel.Account} 登录账号ID：{dbmodel.Id} IP:{_httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString()}"

            //};

            //_userRepository.Change<WebLog>().Insert(newlog);

            #endregion 日志

            #region 设置登录权限

            // token
            var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
        {
            { ClaimConst.Claim_UserId, dbmodel.Id.ToString() }, // 存储Id
            { ClaimConst.Claim_Account, dbmodel.Account ?? "" }, // 存储用户名
            { ClaimConst.Claim_Name, dbmodel.Nick ?? "" } // 存储用户名
        });

            // 获取刷新 token
            var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken); // 第二个参数是刷新 token 的有效期（分钟），默认三十天

            // 设置响应报文头
            App.HttpContext.Response.Headers["access-token"] = accessToken;
            App.HttpContext.Response.Headers["x-access-token"] = refreshToken;

            #endregion 设置登录权限

            #region 获取该用户拥有的菜单

            //var roles=db.Queryable

            #endregion 获取该用户拥有的菜单


            //账号登录成功，移除登录失败缓存数据
            //input.Account.LoginSuccess("AbcApi");



            var entity = dbmodel.Adapt<ApiLoginOut>();

            entity.accessToken = accessToken;
            entity.refreshToken = refreshToken;

            //更新日志信息
            newlog.TableId = dbmodel.Id;



            return entity;

        }
        catch (Exception ex)
        {
            newlog.Status = LoginLogStatus.失败;
            newlog.Result = "登录失败";
            newlog.Exception = ex.Message;


            //任意错误都会导致ip错误计数器自增
            await HandleFailedLogin(newlog.Ip, isIpLock, dbmodel?.Id);
            throw;
        }
        finally
        {
            //更新登录日志
            await sysLogLoginService.NewLoginLog(newlog);

            if (dbmodel != null)
            {
                //最后一次尝试登录的时间
                dbmodel.LastLoginAttempt = DateTime.Now;

                //更新登录时间
                await db.Updateable(dbmodel).ExecuteCommandAsync();
            }
        }
    }


    /// <summary>
    /// ip周期时间
    /// 默认60分钟
    /// </summary>
    private const int IP_CLEANUP_MINUTES = 60;    // 清理间隔(分钟)


    /// <summary>
    /// 当登录失败时
    /// </summary>
    /// <param name="ip"></param>
    /// <param name="isIpLock">是否已经锁ip</param>
    /// <param name="userId">用户id,如果没能获取用户id则为null</param>
    /// <returns></returns>
    private async Task HandleFailedLogin(string ip, bool isIpLock, long? userId)
    {

        //任意登录错误都会到这里
        //获取最新的一条
        var currentTime = DateTime.Now;
        var endTime = DateTime.Now.AddMinutes(-IP_CLEANUP_MINUTES);


        var ipLockList = await db.Queryable<SysUserLock>()
        .Where(l => l.Ip == ip && l.LoginRuleType == LoginRuleType.Ip)
        .Where(l => l.LockStartTime <= currentTime && l.LockEndTime > currentTime)
        .Where(l => l.IsManualUnlock == false) // 未手动解锁
        .Select(l => l.RuleId)
        .ToListAsync();


        //获取所有ip规则
        var ipRuleList = await db.Queryable<SysUserLoginRule>()
            .Where(it => it.LoginRuleType == LoginRuleType.Ip && it.IsEnable == true)
            //如果已经触发了封禁,则不继续累加计数器
            .WhereIF(ipLockList.Count > 0,it=>!ipLockList.Contains(it.Id))
            .ToListAsync();




        //找到现在还在生效的规则
        var attemptLoginList = await db.Queryable<SysLoginAttemptIP>()
            .Where(it => it.Ip == ip)
            .Where(it => it.IsManualUnlock == false)
            .Where(it => it.CreationTime >= endTime)
            //如果已经触发了封禁,则不继续累加计数器
            .WhereIF(ipLockList.Count > 0, it => !ipLockList.Contains(it.RuleId))
            .GroupBy(it => it.RuleId)
            .Select(it => new {
                ruleId = it.RuleId,
                FirstAttemptTime = SqlFunc.AggregateMax(it.FirstAttemptTime),
                FailedAttemptCount = SqlFunc.AggregateMax(it.FailedAttemptCount),
                GroupId = SqlFunc.AggregateMax(it.GroupId),
            })
            .ToListAsync();



        List<SysLoginAttemptIP> insertSysLoginAttemptIPs = [];
        List<SysUserLock> insertSysUserLocks = [];
        foreach (var ipRule in ipRuleList)
        {
            SysLoginAttemptIP lastAttemptLogin = null;
            //判断有没有某一个规则的ip封禁记录
            var tempItem = attemptLoginList.Where(it => it.ruleId == ipRule.Id)
                .FirstOrDefault();
            var attemptId = YitIdHelper.NextId();
            if (tempItem == null)
            {
                //如果之前没有,则新建
                lastAttemptLogin = new SysLoginAttemptIP
                {
                    Id = attemptId,
                    Ip = ip,
                    FirstAttemptTime = DateTime.Now,
                    LastAttemptTime = DateTime.Now,
                    FailedAttemptCount = 1,
                    RuleId = ipRule.Id,
                    GroupId = attemptId
                };
            }
            else
            {
                lastAttemptLogin = new SysLoginAttemptIP
                {
                    Id = YitIdHelper.NextId(),
                    Ip = ip,
                    FirstAttemptTime = tempItem.FirstAttemptTime,
                    LastAttemptTime = DateTime.Now,
                    FailedAttemptCount = tempItem.FailedAttemptCount + 1,
                    RuleId = ipRule.Id,
                    GroupId = tempItem.GroupId
                };
            }

            insertSysLoginAttemptIPs.Add(lastAttemptLogin);


            //判断有没有被锁定
            if (ipRule.MaxFailCount <= lastAttemptLogin.FailedAttemptCount)
            {
                //判断是否已经锁定
                if (!ipLockList.Contains(ipRule.Id))
                {
                    //如果还没有锁定,则执行锁定

                    var lockRecord = new SysUserLock
                    {
                        Id = YitIdHelper.NextId(),
                        LoginRuleType = ipRule.LoginRuleType,
                        LockUserId = userId,
                        LockStartTime = DateTime.Now,
                        LockEndTime = DateTime.Now.AddMinutes(ipRule.LockMinutes),
                        Reason = $"触发[{ipRule.Name}]",
                        Ip = ip,
                        RuleId = ipRule.Id,
                        SysLoginAttemptIPId = lastAttemptLogin.GroupId,
                    };

                    //添加锁定信息,下次再登录就提示次数过多
                    //await db.InsertAsync(lockRecord);
                    insertSysUserLocks.Add(lockRecord);
                }
            }
        }

        if (insertSysLoginAttemptIPs.Count > 0)
        {

            await db.InsertAsync(insertSysLoginAttemptIPs);
        }


        if (insertSysUserLocks.Count > 0)
        {

            await db.InsertAsync(insertSysUserLocks);
        }




    }

    /// <summary>
    /// 检查ip锁定状态
    /// </summary>
    /// <returns></returns>
    private async Task<IPLockInfo> CheckIPLocked(string ip)
    {
        IPLockInfo result = new();
        //检查这个ip在一个周期内触发了多少次失败
        //时间范围


        var currentTime = DateTime.Now;
        var activeLock = await db.Queryable<SysUserLock>()
            .Where(l => l.Ip == ip && l.LoginRuleType == LoginRuleType.Ip)
            .Where(l => l.LockStartTime <= currentTime && l.LockEndTime > currentTime)
            .Where(l => l.IsManualUnlock == false) // 未手动解锁
            .FirstAsync();

        if (activeLock != null)
            result.IsLocked = true;



        return result;
    }

    /// <summary>
    /// Ip锁定信息
    /// </summary>
    private class IPLockInfo
    {
        /// <summary>
        /// 是否锁定
        /// </summary>
        public bool IsLocked { get; set; }

        public long RuleId { get; set; }

    }
    /// <summary>
    /// 触发账号密码错误规则时
    /// </summary>
    /// <param name="user"></param>
    /// <param name="ip"></param>
    /// <returns></returns>
    private async Task HandleFailedAccountWithPwd(SysUser user, string ip)
    {
        // 更新失败计数
        user.LoginFailedCount++;
        //user.LastLoginAttempt = DateTime.Now;
        //await db.Updateable(user).ExecuteCommandAsync();

        // 获取所有启用规则（按触发条件从高到低排序）
        var rule = await db.Queryable<SysUserLoginRule>()
            .Where(r => r.LoginRuleType == LoginRuleType.账号密码 && r.IsEnable == true &&

            //如果失败次数已经比规则中的次数多
            r.MaxFailCount <= user.LoginFailedCount)
            .OrderByDescending(r => r.MaxFailCount)
            .FirstAsync();

        // 检查触发的规则

        if (rule != null)
        {
            // 触发锁定
            var lockRecord = new SysUserLock
            {
                Id = YitIdHelper.NextId(),
                LoginRuleType = rule.LoginRuleType,
                LockUserId = user.Id,
                LockStartTime = DateTime.Now,
                LockEndTime = DateTime.Now.AddMinutes(rule.LockMinutes),
                Reason = $"触发[{rule.Name}]",
                Ip = ip,
                RuleId = rule.Id,
            };


            await db.InsertAsync(lockRecord);


        }

    }
    /// <summary>
    /// 检查用户锁定状态
    /// </summary>
    /// <param name="userId">用户id</param>
    /// <returns></returns>
    private async Task<AccountLockInfo> CheckAccountLocked(long userId)
    {
        var currentTime = DateTime.Now;
        var activeLock = await db.Queryable<SysUserLock>()
            .Where(l => l.LockUserId == userId && l.LoginRuleType == LoginRuleType.账号密码)
            .Where(l => l.LockStartTime <= currentTime && l.LockEndTime > currentTime)
            .Where(l => l.IsManualUnlock == false) // 未手动解锁
            .FirstAsync();

        if (activeLock != null)
        {
            return new AccountLockInfo
            {
                IsLocked = true,
                Reason = activeLock.Reason,
                UnlockTime = activeLock.LockEndTime,
            };
        }
        return new AccountLockInfo
        {
            IsLocked = false,
        };
    }

    /// <summary>
    /// 账号锁定信息
    /// </summary>
    private class AccountLockInfo
    {
        /// <summary>
        /// 是否锁定
        /// </summary>
        public bool IsLocked { get; set; }

        public string Reason { get; set; }

        /// <summary>
        /// 解锁时间
        /// </summary>
        public DateTime? UnlockTime { get; set; }

    }
    /// <summary>
    ///     获取验证码
    /// </summary>
    /// <returns></returns>
    [UnifyResult(typeof(JsonResult))]
    [HttpGet]
    [AllowAnonymous]
    [OperationId(nameof(GetVerifyCode))]
    public VerifyCodeOut GetVerifyCode()
    {
        try
        {
            var verifyKey = Guid.NewGuid().ToString();
            //过期时间5分钟
            var expirySeconds = 300;
            // 设置缓存并指定过期时间
            CacheX.Set($"ApiAccountService_GetVerifyCode_{verifyKey}", verifyKey, TimeSpan.FromSeconds(expirySeconds));

            //生成验证码
            var info = captcha.Generate(verifyKey, expirySeconds);

            using var stream = new MemoryStream(info.Bytes); // 显式释放资源
            var base64File = Convert.ToBase64String(stream.ToArray());

            var base64Src = $"data:image/gif;base64,{base64File}";

            return new VerifyCodeOut { base64Src = base64Src, verifyCodeKey = verifyKey };
        }
        catch (Exception ex)
        {
            throw Oops.Bah("生成验证码失败：" + ex.Message);
        }
    }
}